import { cancellable, truthy, sleep, type CancellablePromise } from '@gnlab/utils'
import { useRefValue } from '@gnlab/utils/env-react'
import { useStreams } from '@/lib/utils'
import services from '@/services'
import { LoadStatus as HistoryStatus } from '@/services/contact-chat'
import type { ChatRecord } from '@/services/contact-chat/definition'
import type { Contact } from '@/services/contacts'

/**
 * 处理聊天记录的加载和滚动
 */
export function useRecordsScrolling(options: {
  contact: Contact
  container: HTMLElement | null // 显示聊天记录并会进行滚动的容器元素
}) {
  const { contact, container } = options
  const contactId = contact.id
  const containerRef = useRefValue(container)

  // 当前聊天基本信息
  const [chatInfo] = useStreams(services.chat.inProgress.info)
  const { initialUnread, focusMsgId, pending } = chatInfo ?? {} // 因企业微信被移除等原因而取消“当前联系人”时，会以此值为空的状态渲染一次

  // 聊天记录加载状态
  const [history] = useStreams(services.chat.history)
  const { status, items: records } = history ?? { status: HistoryStatus.Idle, items: emptyRecords }

  // 工具函数：滚动到聊天记录底部
  const scrollToBottom = useCallback((smooth?: boolean) => {
    // 需等待最新数据渲染出来，才滚动
    setTimeout(() => {
      const container = containerRef.current
      if (container) {
        if (smooth === true)
          container.scrollTo({ left: 0, top: container.scrollHeight, behavior: 'smooth' })
        else container.scrollTo(0, container.scrollHeight)
      }
    })
  }, [])

  /**
   * =========================
   * 处理初始化时加载和跳转
   * =========================
   */
  const [isInitialized, setIsInitialized] = useState(false)
  const initialInfoRef = useRef({ focusMsgId, initialUnread, status })
  useEffect(() => {
    const { focusMsgId, initialUnread, status } = initialInfoRef.current

    // 有 focusMsgId: 跳到这条记录
    // 有“少量”未读消息：跳到第一条未读消息
    if (truthy(focusMsgId) || (truthy(initialUnread) && initialUnread < 100)) {
      const loading = loadTo(truthy(focusMsgId) ? { id: focusMsgId } : { latest: initialUnread! })
      void loading.then(recordId => {
        if (truthy(recordId)) jumpTo(recordId, scrollToBottom)
        else scrollToBottom()
        setIsInitialized(true)
      })
      return () => loading.cancel()
    }

    // 非以上情况，默认滚动到最底部
    // - loading 状态下，此值为正在进行的请求的 promise，需等请求完毕才能跳转
    // - 若已经加载完初次的聊天记录，则直接跳到最后一条记录
    if (status === HistoryStatus.Loading) {
      const loadInfo = services.chat.loadPrevHistory()
      if (loadInfo) {
        const loading = cancellable(loadInfo.promise)
        void loading.then(() => {
          scrollToBottom()
          setIsInitialized(true)
        })
        return () => loading.cancel()
      }
    }
    scrollToBottom()
    setIsInitialized(true)
  }, [])

  /**
   * =========================
   * 处理 focusMsgId 跳转
   * 初始化完成后，若 focusMsgId 变化，跳转到对应的聊天记录
   * =========================
   */
  const isInitializedRef = useRefValue(isInitialized)
  useEffect(() => {
    if (!truthy(focusMsgId) || !isInitializedRef.current) return
    const loading = loadTo({ id: focusMsgId })
    void loading.then(recordId => {
      if (truthy(recordId)) jumpTo(recordId)
    })
    return () => loading.cancel()
  }, [focusMsgId])

  /**
   * =========================
   * 处理 pending 状态
   * 未处于聊天记录底部即为 pending 状态，意味着想要看到早一点到记录，那么也就看不到最新收到的消息。
   * =========================
   */

  // 工具函数：滚动到第一条未读的消息，并重置未读消息数
  const scrollToFirstUnread = useCallback(() => {
    if (!truthy(pending)) return

    // pending 且有未读消息的情况下重置未读消息数并尝试跳到此消息
    if (pending !== true) {
      void services.chat.inProgress.resetPending()

      const record = records.find(r => r.id === pending)
      if (record) return void jumpTo(record.id, () => scrollToBottom(true))
    }

    scrollToBottom(true)
  }, [records, pending])

  /**
   * 根据滚动条位置更新 pending 状态
   */
  useEffect(() => {
    if (!container || !isInitialized) return
    const intervalId = setInterval(() => {
      const { scrollTop, scrollHeight, clientHeight } = container
      const currentAtBottom = scrollTop + 5 >= scrollHeight - clientHeight
      void services.chat.inProgress.markPending(!currentAtBottom)
    }, 300)
    return () => clearInterval(intervalId)
  }, [container, isInitialized])

  /**
   * 滚动条处于顶部时拉取聊天记录
   */
  useEffect(() => {
    if (!container || !isInitialized) return

    let loading: CancellablePromise<void> | null = null
    function load() {
      loading = cancellable<void>(async (cancelled, resolve) => {
        const firstRecordElementId = containerRef.current?.firstElementChild?.id // 因为元素可能重新渲染导致之前的引用失效，所以这里记录 id
        await services.chat.loadPrevHistory()!.promise

        // 修正滚动条位置
        await sleep(100)
        if (cancelled()) return
        const fallback = () => containerRef.current?.scrollTo({ top: 100, behavior: 'smooth' }) // 保证意外情况下，加载完成后也不再处于最顶部，不会再次触发加载
        if (truthy(firstRecordElementId)) jumpTo(firstRecordElementId, fallback, true)
        else fallback()

        // 一次加载完成后，过一段时间才允许触发下一次加载
        await sleep(500)
        resolve()
      })
      void loading.then(() => {
        loading = null
      })
    }

    // 定期检查，处于页面顶部时加载更早的聊天记录
    const intervalId = setInterval(() => {
      if (
        !loading &&
        services.chat.history.value?.status === HistoryStatus.Partial && // loadable
        container.scrollTop < 50 // currentAtTop
      )
        load()
    }, 300)

    return () => {
      if (loading) loading.cancel()
      clearInterval(intervalId)
    }
  }, [container, isInitialized])

  /**
   * 处理新收到或新发送的消息
   */
  const latestRecord = records[records.length - 1] ?? null
  const prevLatestRecordRef = useRef<ChatRecord | null>(null)
  useEffect(() => {
    if (!isInitialized) return // 待初始化完成才执行后续行为

    const prevLatestRecord = prevLatestRecordRef.current
    prevLatestRecordRef.current = latestRecord ?? null
    if (!latestRecord || !prevLatestRecord || latestRecord === prevLatestRecord) return

    // 如果最新消息为自己发送的 滚动到最底部
    if (latestRecord.isSend) return void scrollToBottom()

    // 收到消息，若之前处于视图底部，保持处于底部
    const atBottom = !truthy(services.chat.inProgress.info.value?.pending)
    if (atBottom) return void scrollToBottom()
  }, [isInitialized, latestRecord, contactId])

  return {
    records,
    isLoading: status === HistoryStatus.Loading,
    atBottom: !truthy(pending),
    scrollToFirstUnread,
  }
}

const emptyRecords: ChatRecord[] = []

/**
 * 每个 record 需要指定此函数生成的 ID，以便找到它们进行跳转
 */
export function makeElementId(recordId: string) {
  return `chat-record-${recordId}`
}

/**
 * 工具函数：跳转到指定 id 到聊天记录
 * 若失败，返回 false，并会触发 fallback 回调
 */
function jumpTo(recordOrElementId: string, fallback?: () => void, byElementId = false) {
  // 需等待最新数据渲染出来，才滚动
  setTimeout(() => {
    const elementId = byElementId ? recordOrElementId : makeElementId(recordOrElementId)
    const elm = document.getElementById(elementId)
    if (elm) {
      elm.scrollIntoView(true)
      return
    }
    if (fallback) fallback()
  })
}

/**
 * 工具函数：加载聊天记录，直到找到符合条件到记录
 * 返回 CancellablePromise，可按需终止加载（例如已经离开次联系人的聊天）
 * 若成功找到记录，Promise 会解析出记录的 id，否则解析出 null
 */
// 此函数不可转换为 async，会导致 CancellablePromise 变为普通 Promise
// eslint-disable-next-line @typescript-eslint/promise-function-async
function loadTo(
  conditions: { latest: number } | { id: string }, // 若传入 latest，则返回倒数第 N 条记录；否则返回指定 id 的记录
) {
  // 保证随着不断加载，始终拿到的是最新值
  const history = () => services.chat.history.value!

  const findRecord =
    'latest' in conditions
      ? () =>
          history().items.length >= conditions.latest
            ? history().items[history().items.length - conditions.latest]!
            : null
      : () => history().items.find(r => r.id === conditions.id) ?? null

  return cancellable<string | null>(async (cancelled, resolve) => {
    while (!cancelled()) {
      // 找到了目标记录
      const record = findRecord()
      if (record) return void resolve(record.id)

      // 没找到目标记录，且已加载所有记录，结束运行
      if (history().status === HistoryStatus.Full) {
        return void resolve(null)
      }

      // 加载更多记录
      await services.chat.loadPrevHistory()!.promise // eslint-disable-line no-await-in-loop
    }
  })
}
